/*
 * Copyright (C) 2017 skydoves
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.skydoves.processor;

import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;

import javax.lang.model.util.Elements;
import java.util.ArrayList;
import java.util.List;

import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.STATIC;

public class PreferenceEntityGenerator {
    private static final String CLAZZ_PREFIX = "Preference_";
    private static final String FIELD_PREFERENCE = "preference";
    private static final String FIELD_INSTANCE = "instance";
    private static final String CONSTRUCTOR_CONTEXT = "context";
    private static final String KEY_NAME_LIST = "keyNameList";

    private static final String CLEAR_METHOD = "clear()";
    private static final String APPLY_FLUSH = "flush()";

    private static final String PACKAGE_CONTEXT = "ohos.app.Context";
    private static final String PACKAGE_SHAREDPREFERENCE = "ohos.data.preferences.Preferences";
    private static final String PACKAGE_DATABASEHELPER = "ohos.data.DatabaseHelper";

    private final PreferenceEntityAnnotatedClass annotatedClazz;
    private final Elements annotatedElementUtils;

    public PreferenceEntityGenerator(
            PreferenceEntityAnnotatedClass annotatedClass, Elements elementUtils) {
        this.annotatedClazz = annotatedClass;
        this.annotatedElementUtils = elementUtils;
    }

    public TypeSpec generate() {
        TypeSpec.Builder builder =
                TypeSpec.classBuilder(getClazzName())
                        .addJavadoc(
                                "Generated by PreferenceRoom. (https://github.com/skydoves/PreferenceRoom).\n")
                        .addModifiers(PUBLIC)
                        .superclass(ClassName.get(annotatedClazz.annotatedElement))
                        .addFields(getFieldSpecs());

        if (annotatedClazz.isDefaultPreference) {
            builder.addMethod(getConstructorSpec(true));
        } else {
            builder.addMethod(getConstructorSpec(false));
        }

        builder
                .addMethod(getInstanceSpec())
                .addTypes(getOnChangedTypeSpecs())
                .addFields(getOnChangedFieldSpecs())
                .addMethods(getFieldMethodSpecs())
                .addMethod(getClearMethodSpec())
                .addMethod(getKeyNameListMethodSpec())
                .addMethod(getEntityNameMethodSpec())
                .addMethods(getAddOnChangedListenerSpecs())
                .addMethods(getRemoveOnChangedListenerSpecs())
                .addMethods(getClearOnChangedListenerSpecs());

        return builder.build();
    }

    private List<FieldSpec> getFieldSpecs() {
        List<FieldSpec> fieldSpecs = new ArrayList<>();
        fieldSpecs.add(
                FieldSpec.builder(getSharedPreferencesPackageType(), FIELD_PREFERENCE, PRIVATE, FINAL)
                        .build());
        fieldSpecs.add(FieldSpec.builder(getClassType(), FIELD_INSTANCE, PRIVATE, STATIC).build());
        return fieldSpecs;
    }

    private MethodSpec getConstructorSpec(boolean def) {
        return MethodSpec.constructorBuilder()
                .addModifiers(PRIVATE)
                .addParameter(
                        ParameterSpec.builder(getContextPackageType(), CONSTRUCTOR_CONTEXT)
                                .build())
                .addStatement("$N = new $T($N).getPreferences($S)",
                        FIELD_PREFERENCE,
                        getDataBaseHelperPackageType(),
                        CONSTRUCTOR_CONTEXT,
                        def ? annotatedClazz.packageName : annotatedClazz.entityName)
                .build();
    }

    private MethodSpec getInstanceSpec() {
        return MethodSpec.methodBuilder("getInstance")
                .addModifiers(PUBLIC, STATIC)
                .addParameter(
                        ParameterSpec.builder(getContextPackageType(), CONSTRUCTOR_CONTEXT)
                                .build())
                .addStatement("if ($N != null) return $N", FIELD_INSTANCE, FIELD_INSTANCE)
                .addStatement("$N = new $N($N)", FIELD_INSTANCE, getClazzName(), CONSTRUCTOR_CONTEXT)
                .addStatement("return $N", FIELD_INSTANCE)
                .returns(getClassType())
                .build();
    }

    private List<MethodSpec> getFieldMethodSpecs() {
        List<MethodSpec> methodSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedFields -> {
                    PreferenceFieldMethodGenerator methodGenerator =
                            new PreferenceFieldMethodGenerator(annotatedFields, annotatedClazz, FIELD_PREFERENCE);
                    methodSpecs.addAll(methodGenerator.getFieldMethods());
                });
        return methodSpecs;
    }

    private MethodSpec getClearMethodSpec() {
        return MethodSpec.methodBuilder("clear")
                .addModifiers(PUBLIC)
                .addStatement("$N.$N.$N", FIELD_PREFERENCE, CLEAR_METHOD, APPLY_FLUSH)
                .build();
    }

    private MethodSpec getKeyNameListMethodSpec() {
        MethodSpec.Builder builder =
                MethodSpec.methodBuilder("get" + KEY_NAME_LIST)
                        .addModifiers(PUBLIC)
                        .returns(List.class)
                        .addStatement("List<String> $N = new $T<>()", KEY_NAME_LIST, ArrayList.class);

        this.annotatedClazz.keyNameFields.forEach(
                keyName -> builder.addStatement("$N.add($S)", KEY_NAME_LIST, keyName));

        builder.addStatement("return $N", KEY_NAME_LIST);
        return builder.build();
    }

    private MethodSpec getEntityNameMethodSpec() {
        return MethodSpec.methodBuilder("getEntityName")
                .addModifiers(PUBLIC)
                .returns(String.class)
                .addStatement("return $S", annotatedClazz.entityName)
                .build();
    }

    private List<TypeSpec> getOnChangedTypeSpecs() {
        List<TypeSpec> typeSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedField -> {
                    PreferenceChangeListenerGenerator changeListenerGenerator =
                            new PreferenceChangeListenerGenerator(annotatedField);
                    typeSpecs.add(changeListenerGenerator.generateInterface());
                });
        return typeSpecs;
    }

    private List<FieldSpec> getOnChangedFieldSpecs() {
        List<FieldSpec> fieldSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedField -> {
                    PreferenceChangeListenerGenerator changeListenerGenerator =
                            new PreferenceChangeListenerGenerator(annotatedField);
                    fieldSpecs.add(changeListenerGenerator.generateField(getClazzName()));
                });
        return fieldSpecs;
    }

    private List<MethodSpec> getAddOnChangedListenerSpecs() {
        List<MethodSpec> methodSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedField -> {
                    String onChangeListener =
                            annotatedField.keyName + PreferenceChangeListenerGenerator.CHANGED_LISTENER_POSTFIX;
                    PreferenceChangeListenerGenerator changeListenerGenerator =
                            new PreferenceChangeListenerGenerator(annotatedField);
                    MethodSpec.Builder builder =
                            MethodSpec.methodBuilder("add" + StringUtils.toUpperCamel(onChangeListener))
                                    .addModifiers(PUBLIC)
                                    .addParameter(
                                            ParameterSpec.builder(
                                                    changeListenerGenerator.getInterfaceType(
                                                            getClazzName()), "listener")
                                                    .build())
                                    .addStatement(onChangeListener + ".add(listener)")
                                    .returns(void.class);
                    methodSpecs.add(builder.build());
                });
        return methodSpecs;
    }

    private List<MethodSpec> getRemoveOnChangedListenerSpecs() {
        List<MethodSpec> methodSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedField -> {
                    String onChangeListener =
                            annotatedField.keyName + PreferenceChangeListenerGenerator.CHANGED_LISTENER_POSTFIX;
                    PreferenceChangeListenerGenerator changeListenerGenerator =
                            new PreferenceChangeListenerGenerator(annotatedField);
                    MethodSpec.Builder builder =
                            MethodSpec.methodBuilder("remove" + StringUtils.toUpperCamel(onChangeListener))
                                    .addModifiers(PUBLIC)
                                    .addParameter(
                                            ParameterSpec.builder(
                                                    changeListenerGenerator.getInterfaceType(
                                                            getClazzName()), "listener")
                                                    .build())
                                    .addStatement(onChangeListener + ".remove(listener)")
                                    .returns(void.class);
                    methodSpecs.add(builder.build());
                });
        return methodSpecs;
    }

    private List<MethodSpec> getClearOnChangedListenerSpecs() {
        List<MethodSpec> methodSpecs = new ArrayList<>();
        this.annotatedClazz.keyFields.forEach(
                annotatedField -> {
                    String onChangeListener =
                            annotatedField.keyName + PreferenceChangeListenerGenerator.CHANGED_LISTENER_POSTFIX;
                    MethodSpec.Builder builder =
                            MethodSpec.methodBuilder("clear" + StringUtils.toUpperCamel(onChangeListener) + "s")
                                    .addModifiers(PUBLIC)
                                    .addStatement(onChangeListener + ".clear()")
                                    .returns(void.class);
                    methodSpecs.add(builder.build());
                });
        return methodSpecs;
    }

    private ClassName getClassType() {
        return ClassName.get(annotatedClazz.packageName, getClazzName());
    }

    private String getClazzName() {
        return CLAZZ_PREFIX + annotatedClazz.entityName;
    }

    private TypeName getContextPackageType() {
        return TypeName.get(annotatedElementUtils.getTypeElement(PACKAGE_CONTEXT).asType());
    }

    private TypeName getSharedPreferencesPackageType() {
        return TypeName.get(annotatedElementUtils.getTypeElement(PACKAGE_SHAREDPREFERENCE).asType());
    }

    private TypeName getDataBaseHelperPackageType() {
        return TypeName.get(annotatedElementUtils.getTypeElement(PACKAGE_DATABASEHELPER).asType());
    }
}
